/**
 * 
 * author: liuxu
 * date: 2014-11-18
 *
 * operations about file
 */

package lx.utils.system;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

public class FileUtils {

    // ===============================================================
    // get sub files

    /**
     * get a list of all files and folders under a folder, including itself
     * @param folder
     *            the folder
     * @return the list
     */
    public static List<File> getSubFiles(String folder) {
        return getSubFiles(folder, null, 0, true);
    }

    /**
     * get a list of files and folders under a folder.
     * @param folder
     *            the folder
     * @param filter
     *            a FileFilter to filter files
     * @param depth
     *            only search files at the depth limit. 0 and negative
     *            value will be considered as no limit for depth.
     * @param includeSelf
     *            whether the folder itself should be added
     *            into the list
     * @return the list
     */
    public static List<File> getSubFiles(String folder, FileFilter filter,
            int depth, boolean includeSelf) {
        File folderFile = new File(folder);
        SubFileHandler handler = new SubFileHandler(
                folderFile, filter, depth, includeSelf);
        return handler.getSubFiles();
    }

    private static class SubFileHandler {
        private int mDepthTotal;
        private boolean mIncludeSelf;
        private File mFolder;
        private FileFilter mFilter;

        // use this class to mark depth for each file
        private class FileWrapper {
            int depth;
            File file;

            FileWrapper(File file, int depth) {
                this.file = file;
                this.depth = depth;
            }

            List<FileWrapper> listFileWrapper() {
                if (this.file.isDirectory()) {
                    List<FileWrapper> list = new ArrayList<FileWrapper>();
                    File[] files = this.file.listFiles(mFilter);
                    for (File f : files) {
                        list.add(new FileWrapper(f, this.depth + 1));
                    }
                    return list;
                } else {
                    return null;
                }
            }
        }

        public SubFileHandler(File folder, FileFilter filter,
                int depth, boolean includeSelf) {
            mFolder = folder;
            mFilter = filter;
            mIncludeSelf = includeSelf;

            if (!mFolder.exists()) {
                // should be a folder to go on
                throw new IllegalArgumentException(
                        "not a valid folder: " + folder);
            }

            if (depth <= 0) {
                // consider 0 and negative number as no limit.
                mDepthTotal = Integer.MAX_VALUE;
            } else {
                mDepthTotal = depth;
            }
        }

        public List<File> getSubFiles() {
            List<FileWrapper> list = new ArrayList<FileWrapper>();
            FileWrapper root = new FileWrapper(mFolder, 0);
            doGetSubFiles(root, list);
            if (!mIncludeSelf) {
                // the folder being searched is always the first
                // element in the list
                list.remove(0);
            }
            List<File> retList = new ArrayList<File>();
            for (FileWrapper f : list) {
                retList.add(f.file);
            }
            return retList;
        }

        // recursively add sub files into the list
        private void doGetSubFiles(
                FileWrapper wrapper, List<FileWrapper> list) {
            list.add(wrapper);
            if (wrapper.depth >= mDepthTotal) {
                // folder depth reaches the limit, stop recursion
                return;
            }
            List<FileWrapper> wrapperList = wrapper.listFileWrapper();
            if (wrapperList != null) {
                list.addAll(wrapperList);
                for (FileWrapper f : wrapperList) {
                    doGetSubFiles(f, list);
                }
            }
        }
    }

    // ===============================================================
    // file operations, mostly from android.os.FileUtils

    /**
     * copy a file from srcFile to destFile, return true if succeed,
     * false otherwise.
     */
    public static boolean copyFile(String origin, String target) {
        return copyFile(origin, target, false);
    }

    /**
     * copy a file from srcFile to destFile, return true if succeed,
     * false otherwise.
     * @param origin
     * @param target
     * @param createParent
     *            if set to true, the parent dir of target file will
     *            be created if not exists.
     *            if set to false, when the parent dir does not exists,
     *            the method will just return false.
     * @return true if succeed, false otherwise.
     */

    public static boolean copyFile(String origin, String target, boolean createParent) {
        boolean targetParentExist;
        File srcFile = new File(origin);
        File destFile = new File(target);
        if (!srcFile.exists()) {
            return false;
        }
        File destParent = destFile.getParentFile();
        targetParentExist =
                (destParent == null || !destParent.exists());
        if (!targetParentExist) {
            if (createParent) {
                destParent.mkdirs();
            } else {
                return false;
            }
        }
        return copyFile(srcFile, destFile);
    }

    /**
     * copy a file from srcFile to destFile, return true if succeed,
     * false otherwise.
     * @param srcFile
     * @param destFile
     * @return true if succeed, false otherwise.
     */
    public static boolean copyFile(File srcFile, File destFile) {
        boolean result = false;
        try {
            InputStream in = new FileInputStream(srcFile);
            try {
                result = copyToFile(in, destFile);
            } finally {
                in.close();
            }
        } catch (IOException e) {
            result = false;
        }
        return result;
    }

    /**
     * Copy data from a source stream to destFile.
     * Return true if succeed, return false if failed.
     */
    public static boolean copyToFile(InputStream inputStream, File destFile) {
        try {
            if (destFile.exists()) {
                destFile.delete();
            }
            FileOutputStream out = new FileOutputStream(destFile);
            try {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) >= 0) {
                    out.write(buffer, 0, bytesRead);
                }
            } finally {
                out.flush();
                try {
                    out.getFD().sync();
                } catch (IOException e) {
                }
                out.close();
            }
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Read a text file into a String.
     */
    public static String readTextFile(File file) throws IOException {
        return readTextFile(file, 0, null);
    }

    /**
     * Read a text file into a String, optionally limiting the length.
     * @param file
     *            to read (will not seek, so things like /proc files are OK)
     * @param max
     *            length (positive for head, negative of tail, 0 for no limit)
     * @param ellipsis
     *            to add if the file was truncated (can be null)
     * @return the contents of the file, possibly truncated
     * @throws IOException
     *             if something goes wrong reading the file
     */
    public static String readTextFile(File file, int max, String ellipsis) throws IOException {
        InputStream input = new FileInputStream(file);
        // wrapping a BufferedInputStream around it because when reading /proc
        // with unbuffered
        // input stream, bytes read not equal to buffer size is not necessarily
        // the correct
        // indication for EOF; but it is true for BufferedInputStream due to its
        // implementation.
        BufferedInputStream bis = new BufferedInputStream(input);
        try {
            long size = file.length();
            if (max > 0 || (size > 0 && max == 0)) {
                // "head" mode: read the first N bytes
                if (size > 0 && (max == 0 || size < max)) {
                    max = (int) size;
                }
                byte[] data = new byte[max + 1];
                int length = bis.read(data);
                if (length <= 0) {
                    return "";
                }
                if (length <= max) {
                    return new String(data, 0, length);
                }
                if (ellipsis == null) {
                    return new String(data, 0, max);
                }
                return new String(data, 0, max) + ellipsis;
            } else if (max < 0) {
                // "tail" mode: keep the last N
                int len;
                boolean rolled = false;
                byte[] last = null;
                byte[] data = null;
                do {
                    if (last != null) {
                        rolled = true;
                    }
                    byte[] tmp = last;
                    last = data;
                    data = tmp;
                    if (data == null) {
                        data = new byte[-max];
                    }
                    len = bis.read(data);
                } while (len == data.length);

                if (last == null && len <= 0) {
                    return "";
                }
                if (last == null) {
                    return new String(data, 0, len);
                }
                if (len > 0) {
                    rolled = true;
                    System.arraycopy(last, len, last, 0, last.length - len);
                    System.arraycopy(data, 0, last, last.length - len, len);
                }
                if (ellipsis == null || !rolled) {
                    return new String(last);
                }
                return ellipsis + new String(last);
            } else {
                // "cat" mode: size unknown, read it all in streaming
                // fashion
                ByteArrayOutputStream contents = new ByteArrayOutputStream();
                int len;
                byte[] data = new byte[1024];
                do {
                    len = bis.read(data);
                    if (len > 0) {
                        contents.write(data, 0, len);
                    }
                } while (len == data.length);
                return contents.toString();
            }
        } finally {
            bis.close();
            input.close();
        }
    }

    /**
     * Writes string to file. Basically same as "echo -n $string > $filename"
     * @param filename
     * @param string
     * @throws IOException
     */
    public static void stringToFile(String filename, String string) throws IOException {
        FileWriter out = new FileWriter(filename);
        try {
            out.write(string);
        } finally {
            out.close();
        }
    }

    // ===============================================================

}
